/*
 * ScriptProperties.java
 *
 * Created on July 13, 2007, 11:25 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package filecopier.script;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Set;

/**
 *
 * @author david
 */
public class ScriptProperties implements ScriptPropertiesInterface {

    private Properties scriptProperties = null;
    private File scriptFile = null;
    private ScriptType scriptType = null;
    private int fileCt = 0;
    private int scriptCt = 0;
    private boolean tempScript = false;
    private boolean created = false;

    /**
     * Creates a new instance of ScriptProperties
     */
    public ScriptProperties(final ScriptType scriptType, final File scriptFile) throws ScriptPropertiesException {
        setScriptFile(scriptType, scriptFile);
    }

    public ScriptProperties(final ScriptType scriptType, final File scriptFile, boolean tempScript) throws ScriptPropertiesException {
        setScriptFile(scriptType, scriptFile);
        this.tempScript = tempScript;
    }

    private boolean create() throws ScriptPropertiesException {
        if (scriptFile != null) {
            try {
                created = scriptFile.createNewFile();
            } catch (IOException e) {
                throw new ScriptPropertiesException("An attempt to create \"" + scriptFile + "\" failed because of an IOException.", e, scriptType, scriptFile);
            }
        }
        return created;
    }

    // See if this script file exists and is valid. isValid will return false if the scriptFile is a file
    // that does not exist. It will throw a ScriptPropertiesException if the script file is not valid, scriptFile is null,
    // or it cannot initialize a Properties object for the script file.
    public boolean isValid() throws ScriptPropertiesException {
        boolean valid = false;
        try {
            if (scriptFile.exists()) {
                created = true;
                valid = doSectionsExist();
                if (!valid) {
                    throw new ScriptPropertiesException(scriptFile + " is not a valid " + scriptType.toString() +
                            " Script file.", scriptType, scriptFile);
                }
            }
            return valid;
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("Script file is not valid because scriptFile object is null.", scriptType, scriptFile);
        }
    }

    public ScriptType getScriptType() {
        return scriptType;
    }

    public String getScriptName() {
        return (scriptFile != null ? scriptFile.toString() : null);
    }

    public File getScriptFile() {
        return scriptFile;
    }

    public void setScriptFile(final ScriptType scriptType, final File scriptFile) throws ScriptPropertiesException {
        this.scriptType = scriptType;
        this.scriptFile = scriptFile;
        if (scriptProperties != null) {
            scriptProperties.clear();
        }
        // See if this script file exists and is valid. isValid will return false if the scriptFile is null,
        // or is a file that does not exist. It will throw a ScriptPropertiesException if the script file is not valid
        // or it cannot initialize a Properties object for the script file.
        if (isValid()) {
            // Get the total count of files and scripts in the script file.
            scriptCt = getScriptCt();
            fileCt = scriptType == ScriptType.Restore ? Integer.parseInt(scriptProperties.getProperty("entryCount")) : getFileCt();
        }
    }

    private List<ScriptProperties> getInnerScripts() throws ScriptPropertiesException {
        List<ScriptProperties> innerScripts = null;
        innerScripts = new ArrayList<ScriptProperties>();
        String innerScriptNm = null;
        int i;
        for (i = 1, innerScriptNm = scriptProperties.getProperty("script." + i); innerScriptNm != null;
                i++, innerScriptNm = scriptProperties.getProperty("script." + i)) {
            ScriptProperties innerScript = new ScriptProperties(scriptType, new File(innerScriptNm));
            innerScripts.add(innerScript);
        }
        if (innerScripts.isEmpty()) {
            innerScripts = null;
        }
        return innerScripts;
    }

    public List<File> getFiles() throws ScriptPropertiesException {
        if (scriptType != ScriptType.From && scriptType != ScriptType.To) {
            throw new ScriptPropertiesException("Invalid ScriptType passed to Script.getFiles()", scriptType, scriptFile);
        }
        List<File> files = null;

        // If fileCt is 0 and scriptCt is 0, this is an empty script.
        if (fileCt > 0 || scriptCt > 0) {
            String key = scriptType == ScriptType.From ? "file." : "path.";

            files = new ArrayList<File>();
            // Get files out of all inner scripts.
            List<ScriptProperties> innerScripts = getInnerScripts();
            if (innerScripts != null) {
                for (ScriptProperties s : innerScripts) {
                    List<File> innerFiles = s.getFiles();
                    if (innerFiles != null) {
                        files.addAll(innerFiles);
                    }
                }
            }
            String fileNm = null;
            int i;
            for (i = 1, fileNm = scriptProperties.getProperty(key + i); fileNm != null;
                    i++, fileNm = scriptProperties.getProperty(key + i)) {
                files.add(new File(fileNm));
            }
            if (files.isEmpty()) {
                files = null;
            }
        }
        return files;
    }

    private Properties loadProperties() throws ScriptPropertiesException {
        if (scriptFile == null) {
            throw new ScriptPropertiesException("Script object is not initialized. " +
                    "Call Script.setScriptFile() to initialize.", scriptType, scriptFile);
        }
        Properties properties = null;
        try {
            FileInputStream fis = new FileInputStream(scriptFile);
            properties = new Properties();
            properties.load(fis);
            fis.close();
        } catch (IOException e) {
            throw new ScriptPropertiesException("Attempt to load Properties object failed because of an IOException", e, scriptType, scriptFile);
        }
        return properties;
    }

    public Map<File, File> getFilePairs() throws ScriptPropertiesException {
        if (scriptType != ScriptType.Restore) {
            throw new ScriptPropertiesException("Invalid ScriptType passed to Script.getFilePairs()",
                    scriptType, scriptFile);
        }
        Map<File, File> filePairs = null;

        // If fileCt is 0 and scriptCt is 0, this is an empty script.
        if (fileCt > 0 || scriptCt > 0) {
            filePairs = new TreeMap<File, File>();
            List<ScriptProperties> innerScripts = getInnerScripts();
            if (innerScripts != null) {
                for (ScriptProperties s : innerScripts) {
                    Map<File, File> innerFilePairs = s.getFilePairs();
                    if (innerFilePairs != null) {
                        filePairs.putAll(innerFilePairs);
                    }
                }
            }

            String fileNm1 = scriptProperties.getProperty("file1.restore");
            String fileNm2 = scriptProperties.getProperty("file1.delete");
            for (int i = 1; (fileNm1 != null && fileNm2 != null); i++, fileNm1 = scriptProperties.getProperty("file" + i + ".restore"), fileNm2 = scriptProperties.getProperty("file" + i + ".delete")) {
                filePairs.put(new File(fileNm1), new File(fileNm2));
            }
            if (filePairs.isEmpty()) {
                filePairs = null;
            }
        }

        return filePairs;
    }

    private int getFileCt() throws ScriptPropertiesException {
        int fileCt = 0;
        String[] keys = null;

        if (scriptType == ScriptType.From) {
            for (int i = 1; scriptProperties.getProperty("file." + i) != null; i++, fileCt++) {
                continue;
            }
        } else if (scriptType == ScriptType.To) {
            for (int i = 1; scriptProperties.getProperty("path." + i) != null; i++, fileCt++) {
                continue;
            }
        } else if (scriptType == ScriptType.Restore) {
            int restoreCt = 0;
            int deleteCt = 0;
            for (int i = 1; scriptProperties.getProperty("file" + i + ".restore") != null; i++, restoreCt++) {
                continue;
            }
            for (int i = 1; scriptProperties.getProperty("file" + i + ".delete") != null; i++, deleteCt++) {
                continue;
            }
            if (restoreCt != deleteCt) {
                // If restoreCt is not equal to deleteCt, we have a problem because these entries are supposed to be in pairs.
                // Throw a ScriptPropertiesException.
                throw new ScriptPropertiesException(scriptFile + " is not a valid " + scriptType.toString() +
                        " Script file.", scriptType, scriptFile);
            } else {
                // Set fileCt to the value of restoreCt, deleteCt
                fileCt = restoreCt;
            }
        }

        return fileCt;
    }

    private int getScriptCt() {
        int scriptCt = 0;
        for (int i = 1; scriptProperties.getProperty("script." + i) != null; i++, scriptCt++) {
            continue;
        }
        return scriptCt;
    }

    private int getEntryCt() {
        return (Integer.parseInt(scriptProperties.getProperty("entryCount")));
    }

    private void addNoSave(final String keyPrefix, final String keySuffix, final String value, int ct) throws ScriptPropertiesException {
        if (created == false) {
            // This script file has not been created yet.
            create();
            scriptProperties = new Properties();
            if (tempScript == true) {
                scriptProperties.setProperty("TempScript", "true");
            }
        }
        scriptProperties.setProperty(keyPrefix + ++ct + keySuffix, value);
    }

    private void add(final String keyPrefix, final String keySuffix, final String value, int ct, boolean addEntryCt) throws ScriptPropertiesException {
        try {
            addNoSave(keyPrefix, keySuffix, value, ct);
            if (addEntryCt) {
                scriptProperties.setProperty("entryCount", Integer.toString(ct + 1));
            }
            save();
        } catch (IOException e) {
            // The file was not successfully added to the script file, so don't increment the file count
            // and remove the entry from the properties object.
            scriptProperties.remove(keyPrefix + ct + 1);
            if (addEntryCt) {
                scriptProperties.setProperty("entryCount", Integer.toString(ct));
            }
            throw new ScriptPropertiesException("The script file " + scriptFile + " could not be saved because of an IO error.",
                    e, scriptType, scriptFile);
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to add a file to \"" + scriptFile + "\" failed because a null object was " +
                    " encountered.", e, scriptType, scriptFile);
        }
    }

    private int addMultiples(final String keyPrefix, final String keySuffix, final String[] values, int ct, boolean addEntryCt) throws ScriptPropertiesException {
        int addedCt = 0;
        int startCt = ct;

        try {
            for (String v : values) {
                addNoSave(keyPrefix, keySuffix, v, ct++);
                addedCt++;
            }
            if (addedCt > 0) {
                if (addEntryCt) {
                    scriptProperties.setProperty("entryCount", Integer.toString(ct));
                }
                try {
                    save();
                } catch (IOException e) {
                    ct = startCt + 1;
                    for (; addedCt > 0; addedCt--) {
                        scriptProperties.remove(keyPrefix + (ct++) + keySuffix);
                    }
                    if (addEntryCt) {
                        scriptProperties.setProperty("entryCount", Integer.toString(startCt));
                    }
                    addedCt = 0;
                    throw new ScriptPropertiesException("Attempt to load save \"" + scriptFile +
                            "\" failed because of an IOException", e, scriptType, scriptFile);
                }
            }
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to add files to \"" + scriptFile +
                    "\" failed because a null object was passed to addFiles().", e, scriptType, scriptFile);
        }

        return addedCt;
    }

    public void addFile(final File file) throws ScriptPropertiesException {
        if (scriptType != ScriptType.From && scriptType != ScriptType.To) {
            throw new ScriptPropertiesException("Script.addFile() can only be called for script types of \"From\" or \"To\". " +
                    "Script.addFile() was called on a script of type: \"" + scriptType.toString() + "\"",
                    scriptType, scriptFile);
        }

        String keyPrefix = scriptType == ScriptType.From ? "file." : "path.";
        add(keyPrefix, "", file.toString(), fileCt, false);
        fileCt++;
    }

    public int addFiles(final File[] files) throws ScriptPropertiesException {
        if (scriptType != ScriptType.From && scriptType != ScriptType.To) {
            throw new ScriptPropertiesException("Script.addFile() can only be called for script types of \"From\" or \"To\". " +
                    "Script.addFile() was called on a script of type: \"" + scriptType.toString() + "\"",
                    scriptType, scriptFile);
        }

        String keyPrefix = scriptType == ScriptType.From ? "file." : "path.";

        String[] values = new String[files.length];
        int i = 0;
        for (File file : files) {
            values[i++] = file.toString();
        }

        int addedCt = addMultiples(keyPrefix, "", values, fileCt, false);
        fileCt += addedCt;
        return addedCt;
    }

    public void addScript(final ScriptProperties script) throws ScriptPropertiesException {
        if (scriptType != ScriptType.From && scriptType != ScriptType.To) {
            throw new ScriptPropertiesException("Script.addScript() can only be called for script types of \"From\" or \"To\". " +
                    "Script.addFile() was called on a script of type: \"" + scriptType.toString() + "\"",
                    scriptType, scriptFile);
        }

        add("script.", "", script.getScriptName(), scriptCt, false);
        scriptCt++;
    }

    public int addScripts(final ScriptProperties[] scripts) throws ScriptPropertiesException {
        if (scriptType != ScriptType.From && scriptType != ScriptType.To) {
            throw new ScriptPropertiesException("Script.addScripts() can only be called for script types of \"From\" or \"To\". " +
                    "Script.addFile() was called on a script of type: \"" + scriptType.toString() + "\"",
                    scriptType, scriptFile);
        }

        String[] values = new String[scripts.length];
        int i = 0;
        for (ScriptProperties script : scripts) {
            if (!script.isValid()) {
                throw new ScriptPropertiesException("Script cannot be added to script file \"" + scriptFile + " because " +
                        script.getScriptName() + " does not exist", scriptType, scriptFile);
            }
            if (!script.getScriptType().equals(scriptType)) {
                throw new ScriptPropertiesException("Script cannot be added to script file \"" + scriptFile + " because " +
                        " it does not have the same script type as the script to which it is attempting to be added.",
                        scriptType, scriptFile);
            }
            values[i++] = script.getScriptName();
        }

        int addedCt = addMultiples("script.", "", values, scriptCt, false);
        scriptCt += addedCt;
        return addedCt;
    }

    public void addFilePair(final File key, final File value) throws ScriptPropertiesException {
        if (scriptType != ScriptType.Restore) {
            throw new ScriptPropertiesException("Invalid ScriptType passed to Script.addFilePair(): " + scriptType.toString(),
                    scriptType, scriptFile);
        }

        add("file", ".restore", key.toString(), fileCt, false);
        add("file", ".delete", value.toString(), fileCt, true);
        fileCt++;
    }

    public int addFilePairs(final Map<File, File> filePairs) throws ScriptPropertiesException {
        if (scriptType != ScriptType.Restore) {
            throw new ScriptPropertiesException("Invalid ScriptType passed to Script.addFilePair(): " + scriptType.toString(),
                    scriptType, scriptFile);
        }

        int addedCt = 0;
        int fileCt = this.fileCt;

        int size = filePairs.size();
        String[] restoreFileNames = new String[size];
        String[] deleteFileNames = new String[size];
        int i = 0;

        try {
            Set<Map.Entry<File, File>> s = filePairs.entrySet();
            for (Map.Entry<File, File> me : s) {
                restoreFileNames[i] = me.getKey().toString();
                deleteFileNames[i++] = me.getValue().toString();
            }
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to add file pairs to \"" + scriptFile +
                    "\" failed because a null object was encountered.", e, scriptType, scriptFile);
        }

        addedCt = addMultiples("file", ".restore", restoreFileNames, fileCt, false);
        addedCt = addMultiples("file", ".delete", deleteFileNames, fileCt, true);
        this.fileCt += addedCt;

        return addedCt;
    }

    private void removeNoSave(final String keyPrefix, final String keySuffix, int pos) throws ScriptPropertiesException {
        if ((scriptProperties.remove(keyPrefix + pos + keySuffix)) == null) {
            throw new ScriptPropertiesException("Failed to remove file because file is not in the list.", scriptType, scriptFile);
        }
        // Now, check to see if the file pair was the last in the list. If it is not, we need to adjust the file keys.
        if (pos < fileCt) {
            adjustKeys(keyPrefix, keySuffix);
        }
    }

    private void remove(final Properties fallback, final String keyPrefix, final String keySuffix, int pos, int ct, boolean subtractEntryCt) throws ScriptPropertiesException {
        try {
            removeNoSave(keyPrefix, keySuffix, pos);
            if (subtractEntryCt) {
                scriptProperties.setProperty("entryCount", Integer.toString(ct - 1));
            }
            save();
        } catch (IOException e) {
            scriptProperties = fallback;
            throw new ScriptPropertiesException("An attempt to save \"" + scriptFile + "\" failed because of an IOException.",
                    e, scriptType, scriptFile);
        } catch (ScriptPropertiesException e) {
            scriptProperties = fallback;
            throw e;
        }
    }

    private int removeMultiples(final Properties fallback, final String keyPrefix, final String keySuffix,
            final String[] values, int ct, boolean subtractEntryCt) throws ScriptPropertiesException {
        int removedCt = 0;
        try {
            for (int i = 0; i < values.length; i++) {
                if (values[i] != null) {
                    int pos = getFilePosition(values[i], keyPrefix, keySuffix, fileCt);
                    removeNoSave(keyPrefix, keySuffix, pos);
                    removedCt++;
                }
            }
            if (removedCt > 0) {
                if (subtractEntryCt) {
                    scriptProperties.setProperty("entryCount", Integer.toString(ct - removedCt));
                }
                try {
                    save();
                } catch (IOException e) {
                    throw new ScriptPropertiesException("An attempt to save \"" + scriptFile + "\" failed because of an IOException.",
                            e, scriptType, scriptFile);
                }
            }
        } catch (ScriptPropertiesException e) {
            scriptProperties = fallback;
            throw e;
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to remove files from \"" + scriptFile +
                    "\" failed because a null object was encountered.", e, scriptType, scriptFile);
        }
        return removedCt;
    }

    public void removeFilePair(final File restore, final File delete) throws ScriptPropertiesException {
        if (!isValid()) {
            throw new ScriptPropertiesException("File Pair cannot be removed from \"" + scriptFile + "\" because \"" +
                    scriptFile + "\" does not exist", scriptType, scriptFile);
        }

        try {
            int restorePos = getFilePosition(restore.toString(), "file", ".restore", fileCt);
            int deletePos = getFilePosition(delete.toString(), "file", ".delete", fileCt);
            // restorePos and deletePos will be -1 if the file position could not be found. If they are greater than -1,
            // they should also be equal to each other because the restore and delete files should be a pair. Don't try
            // to remove the file pair is any of these prerequesites are not met.
            if (restorePos == -1) {
                throw new ScriptPropertiesException("File pair cannot be deleted because the restore file \"" + restore + "\" could not " +
                        "be found in the script file.", scriptType, scriptFile);
            }
            if (deletePos == -1) {
                throw new ScriptPropertiesException("File pair cannot be deleted because the delete file \"" + delete + "\" could not " +
                        "be found in the script file.", scriptType, scriptFile);
            }
            if (restorePos != deletePos) {
                throw new ScriptPropertiesException("File pair cannot be deleted because \"" + restore +
                        "\" and \"" + delete + "\" are not a pair.", scriptType, scriptFile);
            }

            // If we make the changes to the Properties object, but fail to save to disk, we will set the scriptProperties
            // object to the cloned object that is a duplicate of what it was before making changes.
            Properties cloned = (Properties) scriptProperties.clone();

            remove(cloned, "file", ".restore", restorePos, fileCt, false);
            remove(cloned, "file", ".delete", deletePos, fileCt, true);
            this.fileCt--;
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to remove a file from \"" + scriptFile + "\" failed because a null object was " +
                    " encountered.", e, scriptType, scriptFile);
        }
    }

    public int removeFilePairs(final Map<File, File> filePairs) throws ScriptPropertiesException {
        if (!isValid()) {
            throw new ScriptPropertiesException("File Pair cannot be removed from \"" + scriptFile + "\" because \"" +
                    scriptFile + "\" does not exist", scriptType, scriptFile);
        }

        // If we make the changes to the Properties object, but fail to save to disk, we will set the scriptProperties
        // object to the cloned object that is a duplicate of what it was before making changes.
        Properties cloned = (Properties) scriptProperties.clone();

        int removedCt = 0;
        try {
            int filePairsCt = filePairs.size();
            String[] restoreValues = new String[filePairsCt];
            String[] deleteValues = new String[filePairsCt];
            int i = 0;
            int fileCt = this.fileCt;

            Set<Map.Entry<File, File>> s = filePairs.entrySet();
            for (Map.Entry<File, File> me : s) {
                restoreValues[i] = me.getKey().toString();
                deleteValues[i++] = me.getValue().toString();
            }

            removedCt = removeMultiples(cloned, "file", ".restore", restoreValues, fileCt, false);
            removedCt = removeMultiples(cloned, "file", ".delete", deleteValues, fileCt, true);
            this.fileCt -= removedCt;
        } catch (NullPointerException e) {
            throw new ScriptPropertiesException("An attempt to remove file pairs to \"" + scriptFile +
                    "\" failed because a null object was passed to removeFilePairs().", e, scriptType, scriptFile);
        }
        return removedCt;
    }

    private void adjustKeys(String keyPrefix, String keySuffix) {
        for (int i = 1; i < fileCt + 1; i++) {
            String value = scriptProperties.getProperty(keyPrefix + i + keySuffix);
            if (value == null) {
                for (int j = i + 1; j < fileCt + 1; j++) {
                    value = scriptProperties.getProperty(keyPrefix + j + keySuffix);
                    if (value != null) {
                        scriptProperties.setProperty(keyPrefix + i + keySuffix, value);
                        scriptProperties.remove(keyPrefix + j + keySuffix);
                        break;
                    }
                }
            }
        }
    }

    private int getFilePosition(final String value, final String keyPrefix, final String keySuffix, int ct) throws ScriptPropertiesException {
        int pos = -1;
        if (keyPrefix != null && keySuffix != null && value != null) {
            for (int i = 1; i < ct + 1; i++) {
                String key = keyPrefix + i + keySuffix;
                String filename = scriptProperties.getProperty(key, "");
                if (filename.equals(value)) {
                    pos = i;
                    break;
                }
            }
        }
        if (pos == -1) {
            throw new ScriptPropertiesException("A file entry named \"" + value +
                    "\" could not be found in the script file \"" + scriptFile + "\".", scriptType, scriptFile);
        }
        return pos;
    }

    private void save() throws IOException {
        FileOutputStream scriptOutStream = new FileOutputStream(scriptFile);
        try {
            scriptProperties.store(scriptOutStream, null);
        } finally {
            scriptOutStream.close();
        }
    }

    private boolean doSectionsExist() throws ScriptPropertiesException {
        scriptProperties = loadProperties();
        boolean sectionsExist = true;

        if (scriptType == ScriptType.From) {
            if ((scriptProperties.getProperty("file.1")) == null) {
                if (((scriptProperties.getProperty("path.1")) != null) ||
                        (((scriptProperties.getProperty("file1.restore")) != null) && (scriptProperties.getProperty("file1.delete")) != null)) {
                    // This appears to be a To ScriptProperties or a Restore ScriptProperties, so it is not a valid From ScriptProperties.
                    sectionsExist = false;
                } else if ((scriptProperties.getProperty("script.1")) == null) {
                    // This script does not have entries for files or for other From scripts, so it is not valid.
                    sectionsExist = false;
                }
            }
        } else if (scriptType == ScriptType.To) {
            if ((scriptProperties.getProperty("path.1")) == null) {
                if (((scriptProperties.getProperty("file.1")) != null) ||
                        (((scriptProperties.getProperty("file1.restore")) != null) && (scriptProperties.getProperty("file1.delete")) != null)) {
                    // This appears to be a From ScriptProperties or a Restore ScriptProperties, so it is not a valid To ScriptProperties.
                    sectionsExist = false;
                } else if ((scriptProperties.getProperty("script.1")) == null) {
                    // This script does not have entries for files or for other To Scripts, so it is not valid.
                    sectionsExist = false;
                }
            }
        } else if (scriptType == ScriptType.Restore) {
            if (((scriptProperties.getProperty("file1.restore")) == null) || ((scriptProperties.getProperty("file1.delete")) == null)) {
                if (((scriptProperties.getProperty("file.1")) != null) || ((scriptProperties.getProperty("path.1")) != null)) {
                    // This appears to be a From ScriptProperties or a To ScriptProperties, so it is not a valid Restore ScriptProperties.
                    sectionsExist = false;
                } else if ((scriptProperties.getProperty("script.1")) == null) {
                    // This script does not have entries for files or for other Restore Scripts, so it is not valid.
                    sectionsExist = false;
                }
            }
        }
        return sectionsExist;
    }
}
